NIUHE

日々私たちが过ごしている日常というのは、実は奇迹の连続なのかもしれんな

TCP/IP 浅析

建立TCP连接的过程

三路握手

TCP用三路握手(three-way handshake)过程创建一个连接。在连接创建过程中,很多参数要被初始化,例如序号被初始化以保证按序传输和连接的强壮性。

一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端打开一个套接字(socket)然后监听来自另一方的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,用户端就能开始创建主动打开(active open)。

  1. 客户端通过向服务器端发送一个SYN来创建一个主动打开,作为三路握手的一部分。客户端把这段连接的序号设定为随机数 A
  2. 服务器端应当为一个合法的SYN回送一个SYN/ACKACK 的确认码应为 A+1SYN/ACK 包本身又有一个随机序号 B
  3. 最后,客户端再发送一个ACK。当服务端受到这个ACK的时候,就完成了三路握手,并进入了连接创建状态。此时包序号被设定为收到的确认号 A+1,而响应则为 B+1

实例参看这里

TCP传输的过程

序列号和确认

在TCP的连接创建状态,两个主机的TCP层间要交换初始序号(ISN:initial sequence number)。这些序号用于标识字节流中的数据,并且还是对应用层的数据字节进行记数的整数。

通常在每个TCP报文段中都有一对序号和确认号。TCP报文发送者认为自己的字节编号为序号,而认为接收者的字节编号为确认号。

TCP报文的接收者为了确保可靠性,在接收到一定数量的连续字节流后才发送确认。这是对TCP的一种扩展,通常称为选择确认(Selective Acknowledgement)。选择确认使得TCP接收者可以对乱序到达的数据块进行确认。每一个字节传输过后,ISN号都会递增1。

通过使用序号和确认号,TCP层可以把收到的报文段中的字节按正确的顺序交付给应用层。序号是32位的无符号数,在它增大到232-1时,便会回绕到0。对于ISN的选择是TCP中关键的一个操作,它可以确保强壮性和安全性。

数据传输实例

打开Wireshark抓包,然后浏览器输入 http://www.liuhe.website,等加载完毕停止抓包。

找到这条HTTP请求,然后Follow TCP stream,得到如图所示界面:

前面三条TCP报文是三路握手建立TCP连接的过程,紧接着客户端发送第一个包含序列号1和766字节数据的报文给服务器。

服务器接收到TCP报文之后返回一条确认报文,该报文内只有报头,没有数据,用确认号767来表示已完全收到。如图所示:

然后服务器继续发送HTTP响应,TCP报文包含序列号1和201字节数据,如下图所示:

客户端以一个没有数据的TCP报文段来回复(只含报头),用确认号202来表示已完全收到并请求下一个报文段。

就这样一直继续下去直到数据传输完毕。


然而当这些数据包都是相连的情况下,接收方没有必要每一次都回应。比如,他收到第1到5条TCP报文段,只需回应第五条就行了。如果第3条TCP报文段被丢失了,尽管他收到了第4和5条,然而他只能回应第2条。 发送方在发送了第三条以后,没能收到回应,因此当时钟(timer)过时(expire)时,他重发第三条。(每次发送者发送一条TCP报文段后,都会再次启动一次时钟:RTT)。

结束连接

连接终止使用了四路握手过程(four-way handshake),在这个过程中每个终端的连接都能独立地被终止。因此,一个典型的拆接过程需要每个终端都提供一对FINACK

不过我在抓包时发现关闭连接时是服务器发送了ACK和FIN,然后客户端返回ACK确认关闭,可能是客户端第一个发出来的FIN没抓到还是怎么回事不太清楚。

##与Unix sock文件的区别

Unix domain socket 或者 IPC socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。与Internet socket不同的是,它不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

Unix domain sockets 使用系统文件的地址来作为自己的身份。它可以被系统进程引用。所以两个进程可以同时打开一个Unix domain sockets来进行通信。不过这种通信方式是发生在系统内核里而不会在网络里传播

TCP数据传输不同于UDP之处

  • 有序数据传输
  • 重发丢失的数据包
  • 舍弃重复的数据包
  • 无错误数据传输
  • 阻塞/流量控制
  • 面向连接(确认有创建三方交握,连接已创建才作传输。)

tcp连接的各种状态

下面为 TCP 状态码列表,以 S 指代服务器,C 指代客户端,S&C 表示两者,S/C 表示两者之一:[1]

  • LISTEN S 等待从任意远程 TCP 端口的连接请求。侦听状态。
  • SYN-SENT C 在发送连接请求后等待匹配的连接请求。通过connect()函数向服务器发出一个同步(SYNC)信号后进入此状态。
  • SYN-RECEIVED S 已经收到并发送同步(SYNC)信号之后等待确认(ACK)请求。
  • ESTABLISHED S&C 连接已经打开,收到的数据可以发送给用户。数据传输步骤的正常情况。此时连接两端是平等的。
  • FIN-WAIT-1 S&C 主动关闭端调用close()函数发出FIN请求包,表示本方的数据发送全部结束,等待TCP连接另一端的确认包或FIN请求包。
  • FIN-WAIT-2 S&C 主动关闭端在FIN-WAIT-1状态下收到确认包,进入等待远程 TCP 的连接终止请求的半关闭状态。这时可以接收数据,但不再发送数据。
  • CLOSE-WAIT S&C 被动关闭端接到FIN后,就发出ACK以回应FIN请求,并进入等待本地用户的连接终止请求的半关闭状态。这时可以发送数据,但不再接收数据。
  • CLOSING S&C 在发出FIN后,又收到对方发来的FIN后,进入等待对方对连接终止(FIN)的确认(ACK)的状态。少见。
  • LAST-ACK S&C 被动关闭端全部数据发送完成之后,向主动关闭端发送FIN,进入等待确认包的状态。
  • TIME-WAIT S/C 主动关闭端接收到FIN后,就发送ACK包,等待足够时间以确保被动关闭端收到了终止请求的确认包。【按照 RFC 793,一个连接可以在 TIME-WAIT 保证最大四分钟,即最大分段寿命(maximum segment lifetime)的2倍】
  • CLOSED S&C 完全没有连接。

TCP状态转换图解 上图描述了 TCP 的11种状态的转换关系。 * 图中的圆角矩形表示状态,箭头表示状态之间的转换。 * 图中用粗线表示客户端主动和被动的服务器端建立连接的正常过程: * 客户端的状态变迁用粗实线 * 服务器端的状态变迁用粗虚线 * 细线用于不常见的序列,如复位、同时打开、同时关闭等。 * 图中的每条状态变换线上均标有“事件/动作”: * 事件是指用户执行了系统调用( CONNECT 、 LISTEN 、 SEND 或 CLOSE )、收到一个报文段( SYN 、 FIN 、 ACK 或 RST )、或者是出现了超过两倍最大的分组生命期的情况; * 动作是指发送一个报文段( SYN 、 FIN 或 ACK )或什么也没有(用“-”表示)。

粗实线表示客户的正常路径;粗虚线表示服务器的正常路径;细线表示不常见的事件。每个连接均开始于 CLOSED 状态。当一方执行了被动的连接原语( LISTEN )或主动的连接原语( CONNECT )时,它便会脱离 CLOSED 状态。如果此时另一方执行了相对应的连接原语,连接便建立了,并且状态变为 ESTABLISHED 。任何一方均可以首先请求释放连接,当连接被释放后,状态又回到了 CLOSED 。

正常状态转换过程

  1. 服务器端首先执行 LISTEN 原语进入被动打开状态( LISTEN ),等待客户端连接;
  2. 当客户端的一个应用程序发出 CONNECT 命令后,本地的 TCP 实体为其创建一个连接记录并标记为 SYN SENT 状态,然后给服务器发送一个 SYN 报文段;
  3. 服务器收到一个 SYN 报文段,其 TCP 实体给客户端发送确认 ACK 报文段同时发送一个 SYN 信号,进入 SYN RCVD 状态;
  4. 客户端收到 SYN + ACK 报文段,其 TCP 实体给服务器端发送出三次握手的最后一个 ACK 报文段,并转换为 ESTABLISHED 状态;
  5. 服务器端收到确认的 ACK 报文段,完成了三次握手,于是也进入 ESTABLISHED 状态。 在此状态下,双方可以自由传输数据。当一个应用程序完成数据传输任务后,它需要关闭 TCP 连接。假设仍由客户端发起主动关闭连接。
  6. 客户端执行 CLOSE 原语,本地的 TCP 实体发送一个 FIN 报文段并等待响应的确认(进入状态 FIN WAIT 1 );
  7. 服务器收到一个 FIN 报文段,它确认客户端的请求发回一个 ACK 报文段,进入 CLOSE WAIT 状态;
  8. 客户端收到确认 ACK 报文段,就转移到 FIN WAIT 2 状态,此时连接在一个方向上就断开了;
  9. 服务器端应用得到通告后,也执行 CLOSE 原语关闭另一个方向的连接,其本地 TCP 实体向客户端发送一个 FIN 报文段,并进入 LAST ACK 状态,等待最后一个 ACK 确认报文段;
  10. 客户端收到 FIN 报文段并确认,进入 TIMED WAIT 状态,此时双方连接均已经断开,但 TCP 要等待一个 2 倍报文段最大生存时间 MSL ( Maximum Segment Lifetime ),确保该连接的所有分组全部消失,以防止出现确认丢失的情况。当定时器超时后, TCP 删除该连接记录,返回到初始状态( CLOSED )。
  11. 服务器收到最后一个确认 ACK 报文段,其 TCP 实体便释放该连接,并删除连接记录,返回到初始状态( CLOSED )。

关于操作系统怎么维护内核还为找到相关资料

实践部分

抓包

关于Wireshark抓包详见这里

Socket 服务器

用python写的简单Socket服务器,实现了如下功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
绑定一个本地端口
在浏览器中访问http://localhost:<your_port>/hello
显示一个hello world

在浏览器中访问http://localhost:<your_port>/info?name=xxx&age=xxx
显示hello, I'm <name_in_the_url> and xxx years old.

通过postman表单提交http://localhost:<your_port>/form
数据
username=xxx
password=xxx
可以简单的存储,也可以不存储
直接在网页上显示出来
I'm <username_posted> and my password is <password_posted>

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class SimpleSocketServer:
def __init__(self, addr, bufsize):
self.s = socket(AF_INET, SOCK_STREAM) # 网络通信, TCP
self.s.bind(addr) # 绑定的IP与端口
self.s.listen(5) # 开始TCP监听, 并指定最多允许多少个客户连接到服务器
self.GET = {} # 存储GET的参数
self.POST = {} # 存储POST的参数
self.bufsize = bufsize

def __del__(self):
self.s.close()

def listen(self):
while True:
self.GET = {} # 初始化为空字典
self.POST = {}
print 'Waiting for connection...'
conn, addr = self.s.accept() # 接受TCP连接,并返回新的套接字与IP地址
print 'Connected by ', addr # 输出客户端的IP地址
data = conn.recv(self.bufsize) # 接收数据
self.response(data, conn)
conn.close()

def response(self, data, conn):
print data
if self.isGET(data): # 判断方法是GET还是POST
str = self.what(data, 'GET') # 获取HTTP请求路径
if str == 'hello':
conn.send('Hello World!')
elif str == 'info':
if not 'name' in self.GET or not 'age' in self.GET:
conn.send('What the fuck !') # 错误信息
else:
conn.send("hello, I'm " + self.GET['name'] + " and "
+ self.GET['age'] + " years old.")
elif self.isPOST(data):
str = self.what(data, 'POST')
if str == 'form':
if not 'username' in self.POST or not 'password' in self.POST:
conn.send('What the fuck !')
else:
conn.send("I'm " + self.POST['username'] + " and my password is " + self.POST['password'] + ".")

def what(self, data, method='GET'):
'''
解析HTTP请求路径和参数,返回路径,参数存在GET或POST字典里
'''
i = 0
# find '/'
while i < len(data):
if data[i] == '/':
break
i += 1
str = ''
i = i + 1
while i < len(data) and data[i] != ' ' and data[i] != '?':
str += data[i]
i += 1
if(data[i] == '?'):
i += 1
while i < len(data) and data[i] != ' ':
key = ''
value = ''
# get key
while i < len(data) and data[i] != ' ' and data[i] != '=':
key += data[i]
i += 1
if data[i] != '=':
break
i += 1
# get value
while i < len(data) and data[i] != ' ' and data[i] != '&':
value += data[i]
i += 1
if value:
self.GET[key] = value
if method == 'POST':
self.POST[key] = value
if data[i] == '&':
i += 1
return str

def isGET(self, data):
'''
判断是否为GET请求
'''
if data[0] == 'G' and data[1] == 'E' and data[2] == 'T':
return True
else:
return False

def isPOST(self, data):
'''
判断是否为POST请求
'''
if data[0] == 'P' and data[1] == 'O':
return True
else:
return False

Powered by Hexo and Theme by Hacker
© 2019 NIUHE